Developing Managed Triggers
Managed triggers are static methods of a .NET class decorated with the SqlTrigger attribute. SqlTrigger has three named parameters:
Event—
A required string-valued parameter that tells SQL Server which type of
trigger you’re defining, as is done when defining T-SQL triggers.
Target— A required string-valued parameter that tells SQL Server which schema and table you’re attaching the trigger to.
Name— An optional string parameter that tells the deployment routine what to call the trigger when it is created in the database.
The implementation contract for a managed trigger is only that it be a static method that returns void.
Inside the method body of a
managed trigger, you need to get a reference to the execution context of
the trigger so you can find out what Data Manipulation Language (DML)
statement the trigger is responding to and which columns have been
updated. You do this by using the SqlContext.TriggerContext object of type SqlTriggerContext. (Note that this object is null when used in nontrigger contexts.) It has the following members:
ColumnCount— An integer property that indicates how many columns were affected by the operation.
IsUpdatedColumn— A Boolean method that indicates whether the column at a specific position was updated during the operation.
TriggerAction— An enum that indicates which operation caused the trigger to fire. For DML triggers, this is either TriggerAction.Insert, TriggerAction.Update, or TriggerAction.Delete. For DDL triggers, the list is quite a bit longer. Refer to MSDN to see all the possible values of the TriggerAction enumeration.
EventData— In the case of a DDL trigger, an object of type SqlXml
that contains an XML document whose content explains the DDL that just
fired. (The XML content model for this object is the same as that
returned by the EVENTDATA() built-in function.)
Have you ever wanted to be
notified by email that some important column value in your tables has
been created or updated? There are many ways to do this, including using
Query Notifications. You can also accomplish this by writing a managed
trigger that calls a web service, which in turn sends an email.
Up until now, you
haven’t had to decrease the runtime safety of your assembly. But because
certain aspects of web services use the Synchronized attribute (which means they do thread synchronization), we have to change our SQLCLR assembly’s permission set to UNSAFE.
Caution
Only the sysadmin role can upload an UNSAFE
assembly to SQL Server. You should allow this uploading only when you
know the code being uploaded doesn’t do anything that might compromise
the integrity of the data, the server, or your job.
First, you need to create a
simple web service routine that sends your email. To do this using
Visual Studio 2008, you create a new local IIS website called photoserve and add to it a new web service called PhotoService.asmx. Then you replace the entire body of PhotoService.cs with the following C# code:
using System;
using System.Web.Services;
using System.Net.Mail;
using System.Configuration;
[WebService(Namespace = "urn:www-samspublishing-com:examples:sqlclr:triggers")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class PhotoService : System.Web.Services.WebService
{
[WebMethod]
public void PhotoUpdateNotify(int ProductPhotoId)
{
MailMessage m = new MailMessage();
m.Subject = "New Photo: " + ProductPhotoId.ToString();
m.From = new MailAddress("ProductPhotoService@localservername");
m.Body = "http://localhost:1347/photoserve/getphoto.aspx?ppid=" +
ProductPhotoId.ToString();
m.To.Add(new MailAddress("PhotoAdmin@ localservername "));
SmtpClient s = new SmtpClient("localservername", 25);
s.Send(m);
}
}
Of course, you
need to have SMTP and IIS correctly configured on your server for this
example to work completely. You also need to replace localhost and localservername and the email account names shown in the code with values that work for you.
Next, you should add a new web form called getphoto.aspx to the site. You replace the entire contents of getphoto.aspx.cs with the code in Listing 9.
Listing 9. A Web Form That Retrieves Photos from SQL Server
using System; using System.Data; using System.Configuration; using System.Web; using System.Data.SqlClient; using System.IO;
public partial class getphoto : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (Request.QueryString["ppid"] != null) { string ppid = Request.QueryString["ppid"].ToString(); string FileName = "photos/" + ppid + ".jpeg"; string MappedFileName = Server.MapPath(FileName); using (SqlConnection c = new SqlConnection( "Data Source=(local);Initial Catalog=AdventureWorks2008; Integrated Security=True" ) ) { using (SqlCommand s = new SqlCommand( @"SELECT LargePhoto FROM Production.ProductPhoto WHERE ProductPhotoId = " + ppid, c)) { c.Open(); using (SqlDataAdapter a = new SqlDataAdapter(s)) { using (DataSet d = new DataSet()) { a.Fill(d); if (d.Tables.Count == 1 && d.Tables[0].Rows.Count == 1) {
byte[] BigImg = (byte[])d.Tables[0].Rows[0]["LargePhoto"]; FileStream f = new FileStream( MappedFileName, FileMode.Create, FileAccess.Write); f.Write(BigImg, 0, BigImg.GetUpperBound(0)); f.Close(); Response.Redirect(FileName, false); } else { Response.Write("<H2>Sorry, ProductPhotoId " + ppid + " was not found.</H2>"); } } } } } } else { Response.Write("<H2>A querystring value for ppid is required.</H2>"); } } }
|
Next, you add a subfolder to the site called photos.
This is the place where the web form will save product photos as JPEG
files and redirect the email recipient. The main body of the code in Listing 46.9 illustrates how to save LOB values to file in a succinct manner, so it may prove useful for your other applications.
You either need to give your ASP.NET user file I/O permissions on photos or have the web application impersonate a user who has those permissions.
To recap, the website code so far consists of the following: a web service (PhotoService.asmx) that generates notification emails containing URLs. These URLs in turn point to a web form (getphoto.aspx) that saves the varbinary value of Production.ProductPhoto.LargePhoto (given a particular ProductPhotoId) to the photos folder as [ProductPhotoId].jpeg.
The last item you need is
the reason you’re writing this code in the first place: a managed
trigger that invokes the web service to kick off the whole process. To
add this, you right-click the SQLCLR project and then select Add,
Trigger. Name this new trigger class Triggers.cs (the default). Then replace the entire content of Triggers.cs with the code in Listing 10.
Listing 10. A Managed Trigger That Invokes a Web Service
using System; using System.Data; using Microsoft.SqlServer.Server; using System.Data.SqlClient; using SQLCLR.photoserve;
public partial class Triggers { [Microsoft.SqlServer.Server.SqlTrigger( Event = "FOR UPDATE", Name = "Production.PhotoUpdateTrigger", Target = "Production.ProductPhoto" )] public static void PhotoUpdateTrigger() { SqlTriggerContext stc = SqlContext.TriggerContext; if (stc.TriggerAction == TriggerAction.Update) { if (stc.IsUpdatedColumn(3)) //The LargePhoto varbinary(max) column { using (SqlCommand s = new SqlCommand( "SELECT DISTINCT ProductPhotoId FROM INSERTED", new SqlConnection("context connection=true"))) { s.Connection.Open(); using (SqlDataReader r = s.ExecuteReader(CommandBehavior.CloseConnection)) { PhotoService p = new PhotoService(); while (r.Read()) { SqlContext.Pipe.Send( "Notifying Web Service of Update for PPID: " + r.GetInt32(0).ToString()); p.PhotoUpdateNotify(r.GetInt32(0)); } } } } } } }
|
Now that all the code is in place, all that’s left is an explanation of the code of PhotoUpdateTrigger() and a test case.
In the code in Listing 46.10, you check to see whether the current TriggerAction is TriggerAction.Update, meaning that the trigger is firing due to an update. You declare this to be true by using the Event named parameter of the SqlTrigger attribute.
Next, you select the ProductPhotoId of the updated row from the INSERTED table and connect to the database by using the context connection.
You execute the command and get your SqlDataReader (r); then you instantiate the PhotoService web service. Using the overloaded method of the Pipe object, you send a string literal informational message (equivalent to T-SQL’s print function), which tells any clients what is about to happen. You call the PhotoUpdateNotify method of the web service and pass in the ProductPhotoId, which in turn sends the email containing the link back to getphoto.aspx, which generates the photo JPEG for that ProductPhotoId.
To make the test case work, you need to make your local machine’s Network Service user a SQL Server login and a user in AdventureWorks2008 with at least db_datareader access. In addition, you might need to use the Visual Studio sgen.exe tool to create a serialization assembly for SQL2008SQLCLR.dll (which sgen.exe would, by default, name SQL2008SQLCLR.XmlSerializers.dll).
You need to load this serialization assembly into AdventureWorks2008 before loading the main assembly (using CREATE ASSEMBLY). (At the time of this writing, it was necessary to also load System.Web.dll and its dependencies into AdventureWorks2008 before loading the application assemblies.)
To test the trigger, you simply update a value of Production.ProductPhoto.LargePhoto:
UPDATE Production.ProductPhoto
SET LargePhoto = LargePhoto
WHERE ProductPhotoId = 69
go
Notifying Web Service of Update for PPID: 69
(1 row(s) affected.)
If you get an email in your
test inbox, you’ve done everything right. If not, don’t fret; this is a
challenging example developed mainly to show the power of managed code.